"""成本分摊"""

import json
import re
from datetime import datetime, date

from odoo import models, api, fields, _
from odoo.tools import float_compare
from odoo.exceptions import UserError, ValidationError
from odoo.addons import decimal_precision as dp
from odoo.tools.float_utils import float_compare

#
# class InvoiceWizardLine(models.TransientModel):
#     _inherit = 'nd.invoice.wizard.line'
#
#     def _need_apportion(self):
#         """计算此明细是否需要分摊成本，
#         如果开票金额 > 未开票金额，则无论是否标记“已全部开票”都需要分摊。
#         如果开票金额 = 未开票金额，则无需分摊。
#         如果开票金额 < 未开票金额，并且标记“已全部开票”，则需要分摊
#         """
#         self.ensure_one()
#         if self.invoice_amount > self.left_amount:
#             return True
#         elif self.invoice_amount < self.left_amount and self.finish:
#             return True
#         else:
#             return False
#
#
# class InvoiceWizard(models.TransientModel):
#     _inherit = 'nd.invoice.wizard'
#
#     @api.multi
#     def confirm(self):
#         """添加成本分摊功能"""
#         res = super().confirm()
#         need_apportion_lines = self.line_ids.filtered(lambda x: x._need_apportion())
#         if need_apportion_lines:
#             self._apportion_lines(need_apportion_lines)
#         return res
#
#     def _apportion_lines(self, apportion_lines):
#         """
#         将超额开具的发票金额分摊到相应的产品成本上去。
#         :param apportion_lines: nd.invoice.wizard.line 需要成本分摊的向导明细
#         :return:
#         """
#         datas = []
#         for apportion_line in apportion_lines:
#             picks = self._get_pick_ids(apportion_line)
#             journal_id = self.env['account.journal'].search([('code', '=', 'COST')], limit=1)
#             expense_product_id = self.env.ref('nd_apportion.apportion_product')
#             data = {
#                 'picking_ids': [(6, 0, picks.ids)],
#                 'pre_invoice_line_id': apportion_line.invoice_line.id,
#                 'account_journal_id': journal_id.id,
#                 'cost_lines': [(0, 0, {
#                     'product_id': expense_product_id.id,
#                     'name': expense_product_id.name or '',
#                     'split_method': expense_product_id.split_method or 'equal',
#                     'price_unit': apportion_line.invoice_amount - apportion_line.left_amount,
#                     'account_id': expense_product_id.property_account_expense_id.id or expense_product_id.categ_id.property_account_expense_categ_id.id,
#                 })]
#             }
#             datas.append(data)
#         cost_ids = self.env['stock.landed.cost'].create(datas)
#         cost_ids.compute_landed_cost()
#         cost_ids.with_context(nd_apportion=True).button_validate()
#
#     def _get_pick_ids(self, apportion_lines):
#         picking_ids = self.env['stock.picking']
#         for line in apportion_lines:
#             pre_invoice_line = line.invoice_line
#             if pre_invoice_line.move_id:
#                 picking_ids |= pre_invoice_line.move_id.picking_id
#             elif pre_invoice_line.sale_line_id.move_ids:
#                 picking_ids |= pre_invoice_line.sale_line_id.move_ids.mapped('picking_id')
#             elif pre_invoice_line.purchase_line_id.move_ids:
#                 picking_ids |= pre_invoice_line.purchase_line_id.move_ids.mapped('picking_id')
#
#         return picking_ids


class AccountInoivceLine(models.Model):
    _inherit = 'account.invoice.line'

    def _need_apportion(self, cancel=False):
        """计算此明细是否需要分摊成本，
        开票金额为此明细上的未税金额，
        未开票金额 = 对应的原发票明细(pre.account.line)上总未税金额 - 对应的原发票明细(pre.account.line)上已开票金额-本明细上未税金额
        如果开票金额 > 未开票金额，则需要分摊。
        如果开票金额 = 未开票金额，则无需分摊。
        如果开票金额 < 未开票金额，并且发票明细(pre.account.line)状态为“已开票”，则需要分摊
        """
        self.ensure_one()
        if self.quantity > 0:
            apportion_amount, _ = self._get_apportion_amount(cancel=cancel)
            return float_compare(apportion_amount, 0, 2) != 0
        return False

    def _get_apportion_amount(self, cancel=False):
        """获取待摊金额，待摊金额可能为正也可能为负值, cancel表示用于取消发票的情况
        返回 （待摊金额，对应的调拨单IDS）
        """
        # 分摊信息格式：{product_id: [[[picking_ids], value1, percent1, line_id1], [[picking_ids], value2, percent2, line_id1]]}
        self.ensure_one()
        if cancel:
            # 取消发票时，直接获取当前发票上的分摊记录信息
            if self.invoice_id.type in ['in_invoice', 'in_refund']:
                this_invoice_apportion_info_dict = self.invoice_id._get_apportion_info(self.invoice_id, flag='this')
                product_id = str(self.product_id.id)
                if product_id in this_invoice_apportion_info_dict:
                    for line_info in this_invoice_apportion_info_dict[product_id]:
                        if self.id == line_info[3]:
                            return line_info[1] * -1, line_info[0]
                return 0, []
            else:
                raise UserError('只有供应商发票、供应商红字发票才可能需要成本分摊！')
        else:
            if self.invoice_id.type in ['in_invoice']:
                # 对于正常发票，根据”预发票明细“上未开票金额 和 本发票明细上开票金额进行对比，以决定待分摊金额
                pre_account_line = self.pre_account_line_id
                pre_account_line_done = pre_account_line.state == 'invoiced'  # TODO 由于红冲、取消等功能，用状态来判断可能不稳定
                this_invoice_amount = self.price_subtotal
                uninvoiced_amount = pre_account_line.business_untaxed_amount - pre_account_line.invoiced_amount + this_invoice_amount
                if this_invoice_amount > uninvoiced_amount:
                    # 正值，多开
                    return this_invoice_amount - uninvoiced_amount, False
                elif this_invoice_amount < uninvoiced_amount and pre_account_line_done:
                    # 负值，少开
                    return this_invoice_amount - uninvoiced_amount, False
                else:
                    return 0, False
            elif self.invoice_id.type in ['in_refund']:
                # 对于红冲发票，则看原发票上对应产品是否有成分分摊，有，则根据金额占比来计算本次红冲需要反向分摊的金额
                origin_invoice_apportion_info_dict = self.invoice_id._get_apportion_info(self.invoice_id, flag='pre')
                product_id = str(self.product_id.id)
                if product_id in origin_invoice_apportion_info_dict:
                    # 记录里一个产品通常只对应一个记录，但是当一个发票明细里有多个相同产品并都发生了分摊时，就会有多个记录
                    # 这种我们就根据发票明细行的pre_account_line_id来匹配
                    for info_list in origin_invoice_apportion_info_dict[product_id]:
                        origin_account_invoice_line_id = self.env['account.invoice.line'].browse(int(info_list[3]))
                        if self.pre_account_line_id and self.pre_account_line_id == origin_account_invoice_line_id.pre_account_line_id:
                            percent = info_list[2]
                            if info_list[1] <= 0:
                                return self.price_subtotal * percent, info_list[0]
                            else:
                                return self.price_subtotal * percent * -1, info_list[0]
                else:
                    return 0, []
            else:
                raise UserError('只有供应商发票、供应商红字发票才可能需要成本分摊！')


class AccountInvoice(models.Model):
    _inherit = 'account.invoice'

    nd_apportion_info = fields.Text('分摊信息')

    @api.multi
    def check_lc(self):
        action = self.env.ref('stock_landed_costs.action_stock_landed_cost').read()[0]
        action['domain'] = [('apportion_invoice_id', 'in', self.ids)]
        return action

    @api.multi
    def action_invoice_open(self):
        """正常发票验证的时候，如果需要分摊成本，则执行分摊
            红冲发票验证时，如果原发票有分摊，则要执行反向分摊"""
        for invoice in self:
            if invoice.type in ['in_invoice']:
                # 如果是正常采购发票，直接进行分摊逻辑
                invoice_lines_need_apportion = invoice.invoice_line_ids.filtered(lambda x: x._need_apportion())
                if invoice_lines_need_apportion:
                    self._apportion_lines(invoice_lines_need_apportion)
            elif invoice.type in ['out_invoice', 'out_refund']:
                # 销售相关发票不存在分摊
                continue
            elif invoice.type in ['in_refund']:
                # 如果原发票有分摊，则红票需要反向红冲
                # 需要先获取此发票明细对于的原发票是否有分摊，这里直接按照产品的分摊金额来算（不管分摊数量）
                invoice_lines_need_apportion = invoice.invoice_line_ids.filtered(lambda x: x._need_apportion())
                if invoice_lines_need_apportion:
                    self._apportion_lines(invoice_lines_need_apportion)
            else:
                raise UserError(f'不能识别的发票类型：{invoice.type}')
        return super().action_invoice_open()

    @api.multi
    def action_invoice_cancel(self):
        """取消发票的同时, 如果是正常发票且有分摊，则需要反向红冲。
            对于红票，如果本身有分摊，则执行反向红冲"""
        for invoice in self:
            if invoice.type in ['out_invoice', 'out_refund']:
                # 销售相关发票，没有分摊相关逻辑
                continue
            elif invoice.type in ['in_invoice', 'in_refund']:
                # 采购正常/红冲发票取消，如果本身有分摊，则需要反向分摊
                invoice_lines_need_apportion = invoice.invoice_line_ids.filtered(lambda x: x._need_apportion(cancel=True))
                if invoice_lines_need_apportion:
                    self._apportion_lines(invoice_lines_need_apportion, cancel=True)
                    invoice.nd_apportion_info = False       # 清除分摊信息
            else:
                raise UserError(f'不能识别的发票类型：{invoice.type}')
        return super().action_invoice_cancel()

    @api.model
    def _apportion_lines(self, apportion_lines, cancel=False):
        """
        将超额开具的发票金额分摊到相应的产品成本上去。
        :param apportion_lines: account.invoice.line 需要成本分摊的发票明细
        :param cancel: 当发票取消时需要分摊，此参数未True
        :return:
        """
        datas = []

        for apportion_line in apportion_lines:
            journal_id = self.env['account.journal'].search([('code', '=', 'COST')], limit=1)
            expense_product_id = self.env.ref('nd_apportion.apportion_product')
            price_unit, picks = apportion_line._get_apportion_amount(cancel=cancel)
            if picks is False:
                picks = self._get_pick_ids(apportion_line).ids
            landed_date = apportion_line.invoice_id.date_invoice or fields.Date.context_today(self)
            data = {
                'picking_ids': [(6, 0, picks)],
                'date': landed_date,
                'pre_invoice_line_id': apportion_line.pre_account_line_id.id,
                'apportion_invoice_id': apportion_line.invoice_id.id,
                'account_journal_id': journal_id.id,
                'nd_invoice_line_id': apportion_line.id,
                'cost_lines': [(0, 0, {
                    'product_id': expense_product_id.id,
                    'name': expense_product_id.name or '',
                    'split_method': expense_product_id.split_method or 'equal',
                    'price_unit': price_unit,
                    'account_id': expense_product_id.property_account_expense_id.id or expense_product_id.categ_id.property_account_expense_categ_id.id,
                })]
            }
            datas.append(data)
        cost_ids = self.env['stock.landed.cost'].create(datas)
        cost_ids.compute_landed_cost()
        cost_ids.with_context(nd_apportion=True).button_validate()
        self._record_apportion_info(cost_ids)

    @api.model
    def _record_apportion_info(self, cost_ids):
        """记录分摊信息到发票上，形如:
        {product_id: [[[picking_ids], value1, percent1, line_id1], [[picking_ids], value2, percent2, line_id2]]}
        cost_ids: stock.landed.cost，已经完成的到岸成本分摊
        """
        for cost in cost_ids:
            invoice_id = cost.apportion_invoice_id
            if invoice_id:
                total_invoice_value = cost.nd_invoice_line_id.price_subtotal
                apportion_info = {}
                if invoice_id.nd_apportion_info:
                    apportion_info = json.loads(invoice_id.nd_apportion_info)
                for line in cost.valuation_adjustment_lines:
                    # 这里记录分摊金额在发票明细金额里的占比，目的是在后续的（红字发票）里根据此占比来进行反向分摊
                    try:
                        additional_value_percent = abs(line.additional_landed_cost) / total_invoice_value
                    except ZeroDivisionError:
                        additional_value_percent = 0
                    # 注意，当发票取消时，执行了反向分摊后，这里的分摊信息应当被清除，但是为了逻辑简单，这里仍然记录，在发票取消方法里进行清除
                    result = apportion_info.setdefault(str(line.product_id.id), [])
                    result.append([cost.picking_ids.ids, line.additional_landed_cost, additional_value_percent, cost.nd_invoice_line_id.id])
                apportion_info_str = json.dumps(apportion_info)
                invoice_id.nd_apportion_info = apportion_info_str

    @api.model
    def _get_apportion_info(self, invoice, flag):
        """
        获取发票已红冲信息
        :param invoice: account.invoice, 要获取分摊信息的发票
        :param flag: str，’this‘、’pre‘分别代表获取本发票、原发票（红字发票的情况）的分摊信息
        :return:
        """
        invoice.ensure_one()
        if flag == 'this':
            origin_invoice_id = invoice
        elif flag == 'pre':
            origin_invoice_id = invoice.refund_invoice_id
        else:
            raise UserError(f'不能识别的类型值：{flag}')
        apportion_info_dict = {}
        if origin_invoice_id.nd_apportion_info:
            apportion_info_dict = json.loads(origin_invoice_id.nd_apportion_info)
        return apportion_info_dict

    def _get_pick_ids(self, apportion_lines):
        """
        获取发票明细对应的入库单
        :param apportion_lines: account.invoice.line 需要成本分摊的发票明细
        :return: stock.picking
        """
        picking_ids = self.env['stock.picking']
        for line in apportion_lines:
            pre_invoice_line = line.pre_account_line_id
            if pre_invoice_line.move_id:
                picking_ids |= pre_invoice_line.move_id.picking_id
            elif pre_invoice_line.sale_line_id.move_ids:
                picking_ids |= pre_invoice_line.sale_line_id.move_ids.mapped('picking_id')
            elif pre_invoice_line.purchase_line_id.move_ids:
                picking_ids |= pre_invoice_line.purchase_line_id.move_ids.mapped('picking_id')

        return picking_ids


class LandedCost(models.Model):
    _inherit = 'stock.landed.cost'

    pre_invoice_line_id = fields.Many2one('pre.account.line', string="待分摊的发票明细")
    apportion_invoice_id = fields.Many2one('account.invoice', string="待分摊的发票")
    # 在_apportion_lines方法里我们确保了一条发票明细的分摊对应单独的一张到岸成本
    nd_invoice_line_id = fields.Many2one('account.invoice.line', string="发票明细",
                                         help="记录每个分摊明细对应的发票明细，便于后续记录分摊金额的占比")

    def get_valuation_lines(self):
        """筛选出和待分摊发票明细产品相同的成本明细行"""
        lines = super().get_valuation_lines()
        if self.pre_invoice_line_id:
            new_lines = []
            for d in lines:
                # 这个move_id的比较和break的原因是，一个入库单上有多个相同产品时，会产生形如：
                # {"599": [[[264], 33.34, 0.1667, 134], [[264], 33.34, 0.1667, 134], [[264], 33.34, 0.1667, 134]}
                # 这种一条发票明细多次分摊的记录，进而导致后续的反向分摊等金额出错。有两种处理措施，1是在后续的反向分摊时汇总这些金额，
                # 2是在分摊的时候就避免这种一条明细多次分摊的情况。这里实现的是后者。
                # TODO 这里依赖于每个预发票明细都有对应库存移动，这又依赖于产品的开票策略设置为”收到货数量“
                if d.get('product_id') == self.pre_invoice_line_id.product_id.id and d.get('move_id') == self.pre_invoice_line_id.move_id.id:
                    new_lines.append(d)
                    break
            return new_lines
        else:
            return lines


class AdjustmentLines(models.Model):
    _inherit = 'stock.valuation.adjustment.lines'

    def _create_account_move_line(self, move, credit_account_id, debit_account_id, qty_out, already_out_account_id):
        """覆盖系统原生的，来修改相关分摊科目"""
        if self.env.context.get('nd_apportion'):
            AccountMoveLine = []

            base_line = {
                'name': self.name,
                'product_id': self.product_id.id,
                'quantity': 0,
                'partner_id': self.move_id.picking_id and self.move_id.picking_id.partner_id.id,
            }

            if self.move_id._is_in():
                in_out = 'in'
            elif self.move_id._is_out():
                in_out = 'out'
            else:
                in_out = False
            # debit_acc_id, credit_acc_id = self.env['product.category']._nd_get_apportion_account(self.move_id, self.price_unit>0)
            debit_acc_id, credit_acc_id = self.env['product.category']._nd_get_apportion_account(self.move_id,
                                                                                                 # self.cost_line_id.price_unit > 0,
                                                                                                 in_out=in_out)
            debit_line = dict(base_line, account_id=debit_acc_id.id)
            credit_line = dict(base_line, account_id=credit_acc_id.id)
            diff = self.additional_landed_cost
            if diff > 0:
                debit_line['debit'] = diff
                credit_line['credit'] = diff
            else:
                # negative cost, reverse the entry
                debit_line['credit'] = -diff
                credit_line['debit'] = -diff
            AccountMoveLine.append([0, 0, debit_line])
            AccountMoveLine.append([0, 0, credit_line])

            # Create account move lines for quants already out of stock
            if qty_out > 0:
                debit_acc_id, credit_acc_id = self.env['product.category']._nd_get_apportion_account(self.move_id,
                                                                                                     # self.cost_line_id.price_unit > 0,
                                                                                                     in_out='out')
                debit_line = dict(base_line,
                                  name=(self.name + ": " + str(qty_out) + _(' already out')),
                                  quantity=0,
                                  account_id=debit_acc_id.id)
                credit_line = dict(base_line,
                                   name=(self.name + ": " + str(qty_out) + _(' already out')),
                                   quantity=0,
                                   account_id=credit_acc_id.id)
                diff = diff * qty_out / self.quantity
                if diff > 0:
                    debit_line['debit'] = diff
                    credit_line['credit'] = diff
                else:
                    # negative cost, reverse the entry
                    debit_line['credit'] = -diff
                    credit_line['debit'] = -diff
                AccountMoveLine.append([0, 0, debit_line])
                AccountMoveLine.append([0, 0, credit_line])

                # uninvoiced_info: {出库ID：[出库单在此入库单上减去的数量（未开票部分），出库单在此入库单上减去的价值（未开票部分）]}
                uninvoiced_info = self.env['stock.move']._get_uninvoiced_out_move(self.move_id)
                already_out_and_uninvoice_qty = sum([out_info[0] for out_info in uninvoiced_info.values()])
                already_out_and_invoice_qty = qty_out - already_out_and_uninvoice_qty
                # 如果有未开票的，需要更新出库stock.move的price_unit和landed_cost_value
                if already_out_and_uninvoice_qty > 0:
                    for out_id, value_info in uninvoiced_info.items():
                        land_value = diff / qty_out * value_info[0]     # 待摊金额
                        out_move_id = self.env['stock.move'].browse(int(out_id))
                        land_price = land_value / out_move_id.quantity_done
                        out_move_id_price_unit = out_move_id.price_unit
                        # 理论上这里都是出库，所以price_unit都是负值，但保险起见。。。
                        if out_move_id_price_unit < 0:
                            data = {'price_unit': -(abs(out_move_id_price_unit) + land_price),
                                   'landed_cost_value': -(abs(out_move_id.landed_cost_value) + land_value)}
                        else:
                            data = {'price_unit': out_move_id_price_unit + land_price,
                                    'landed_cost_value': out_move_id.landed_cost_value + land_value}
                        out_move_id.write(data)

                # 如果有已开票的，才进行成本结转
                if already_out_and_invoice_qty > 0 and self.env.user.company_id.anglo_saxon_accounting:
                    debit_acc_id, credit_acc_id = self.env['product.category']._nd_get_apportion_account(self.move_id,
                                                                                                         # self.cost_line_id.price_unit > 0,
                                                                                                         in_out='anglo')
                    debit_line = dict(base_line,
                                      name=(self.name + ": " + str(qty_out) + _(' already out')),
                                      quantity=0,
                                      account_id=credit_acc_id.id)

                    credit_line = dict(base_line,
                                       name=(self.name + ": " + str(qty_out) + _(' already out')),
                                       quantity=0,
                                       account_id=debit_acc_id.id)

                    # new_diff是考虑到已开票的出库数量对应的分摊金额，而不是单纯的出库数量对应的分摊金额
                    new_diff = already_out_and_invoice_qty * diff / qty_out
                    if diff > 0:
                        debit_line['debit'] = new_diff
                        credit_line['credit'] = new_diff
                    else:
                        # negative cost, reverse the entry
                        debit_line['credit'] = -new_diff
                        credit_line['debit'] = -new_diff
                    AccountMoveLine.append([0, 0, debit_line])
                    AccountMoveLine.append([0, 0, credit_line])

            return AccountMoveLine
        else:
            return super()._create_account_move_line(move, credit_account_id, debit_account_id, qty_out, already_out_account_id)


class Categ(models.Model):
    _inherit = 'product.category'

    @api.model
    def _nd_get_apportion_account(self, move_id, in_out):
        """
        根据开票向导明细来获取分摊科目
        :param move_id: stock.move 导致分摊的库存移动
        :return:
        """
        # move_id = apportion_line.invoice_line.move_id
        # if not move_id:
        #     if apportion_line.invoice_line.sale_line_id:
        #         move_id = apportion_line.invoice_line.sale_line_id.move_ids
        #     elif apportion_line.invoice_line.purchase_line_id:
        #         move_id = apportion_line.invoice_line.purchase_line_id.move_ids

        # if not move_id:
        #     raise UserError(f'存在待分摊的产品“{apportion_line.invoice_line.product_id.display_name}”，但这个产品还没有入库！')
        if not move_id:
            raise UserError('没有传入库存移动')
        positive = True # 到岸成本本身就有对正负值的处理，这里无需再分正负
        product_categ_id = move_id.product_id.categ_id
        # positive = apportion_line.invoice_amount > apportion_line.left_amount
        if in_out == 'in':
            if positive:
                debit_acc_id = product_categ_id.property_stock_valuation_account_id
                credit_acc_id = product_categ_id.property_stock_account_input_categ_id
            else:
                debit_acc_id = product_categ_id.property_stock_account_input_categ_id
                credit_acc_id = product_categ_id.property_stock_valuation_account_id
        elif in_out == 'out':
            if positive:
                debit_acc_id = product_categ_id.property_stock_account_output_categ_id
                credit_acc_id = product_categ_id.property_stock_valuation_account_id
            else:
                debit_acc_id = product_categ_id.property_stock_valuation_account_id
                credit_acc_id = product_categ_id.property_stock_account_output_categ_id
        elif in_out == 'anglo':
            if positive:
                debit_acc_id = product_categ_id.property_stock_account_output_categ_id
                credit_acc_id = product_categ_id.property_account_expense_categ_id
            else:
                debit_acc_id = product_categ_id.property_account_expense_categ_id
                credit_acc_id = product_categ_id.property_stock_account_output_categ_id
        # elif move_id.picking_code == 'internal':
        #     raise UserError(f'产品“{move_id.product_id.display_name}”的内部调拨不能进行分摊')
        else:
            raise UserError(f'不能识别的调拨方式{in_out}')

        return debit_acc_id, credit_acc_id



